package com.guickcheck.test;

import static com.google.inject.internal.Preconditions.checkNotNull;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.List;
import java.util.Set;

import junit.framework.TestCase;

import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.inject.Inject;
import com.google.inject.Key;
import com.google.inject.internal.Iterables;
import com.google.inject.internal.Nullable;
import com.guickcheck.inject.TestInjector;

public class GuickTestCase extends TestCase {
  final Method method;
  final Object target;

  @Inject
  TestInjector injector;

  public GuickTestCase(@Nullable Object target, Method method) {
    this.method = checkNotNull(method);
    this.target = target;
  }

  @Override
  protected void runTest() throws Throwable {
    Type[] parameterTypes = method.getGenericParameterTypes();
    Object[] arguments = new Object[parameterTypes.length];
    for (int i = 0; i < arguments.length; i++) {
      arguments[i] = injector.getInstance(Key.get(parameterTypes[i]));
    }
    try {
      method.invoke(target, arguments);
    } catch (Throwable thrown) {
      tryShrinking(parameterTypes, arguments, thrown);
    }
  }

  @SuppressWarnings("unchecked")
  private void tryShrinking(Type[] parameterTypes, Object[] arguments,
      Throwable thrown) throws Throwable {
    // Guaranteed to throw something.

    List<Iterable<Object>> shrunkenArguments = Lists.newArrayList();
    for (int i = 0; i < arguments.length; i++) {
      Iterable<Object> shrunk =
          injector.shrink((Key) Key.get(parameterTypes[i]), arguments[i]);
      shrunk = concat(shrunk, arguments[i]);
      shrunkenArguments.add(shrunk);
    }

    Iterable<List<Object>> shrinks = product(shrunkenArguments);
    for (List<Object> shrink : shrinks) {
      List<Object> shrinkArgs = shrink;
      try {
        method.invoke(target, shrinkArgs.toArray());
      } catch (Throwable t) {
        boolean changed = false;
        for (int i = 0; i < arguments.length && !changed; i++) {
          changed |= arguments[i] != shrinkArgs.get(i);
        }
        if (changed) {
          tryShrinking(parameterTypes, shrinkArgs.toArray(), t);
          return;
        }
      }
    }
    throw thrown;
  }

  private static <T> Iterable<T> concat(Iterable<T> rest, T t) {
    return Iterables.concat(rest, Collections.singletonList(t));
  }

  private static <T> Iterable<List<T>> product(Iterable<Iterable<T>> axes) {
    List<Set<T>> theAxes = Lists.newArrayList();
    for (Iterable<T> axis : axes) {
      Set<T> set = Sets.newLinkedHashSet();
      for (T elem : axis) {
        set.add(elem);
      }
      theAxes.add(set);
    }
    return Sets.cartesianProduct(theAxes);
  }
}
